// Ir_DAKIN_IOT.c 在 AVR ATtiny13A单片机上 之编译说明
// Compiler: avr-gcc (AVR_8_bit_GNU_Toolchain_3.7.0_1796) 7.3.0
// Target : MCU=attiny13a; Crystal=1.2000Mhz (使用片内RC振荡器，出厂默认值)
// Fuse: LowByte=0x2A, HighByte=0xFF, ExtByte=0xFF, 芯片默认值基础上保留EEPROM
// 发布日期: 2025-9-3, 抗战胜利暨反法西斯战争胜利80周年大阅兵。
// copyrights, 2010~2025, maidoo@163.com  麦兜-CDMA650 欢乐在 数码之家

/* ========================================================================
        大金中央空调面板控制器，加装支持IOT场景（固开、固关）的红外遥控
   ========================================================================
    AVR ATtiny13单片机    _____   _____
            Reset  PB5 -|1    \_/    8|- VCC
       POW_AN I/O  PB3 -|2   AT      7|- PB2   POW_SEN IN
         BEEP OUT  PB4 -|3   Tiny13  6|- PB1   INT0红外输入
                   GND -|4___________5|- PB0   FAN_AN I/O

    这是个附加到大金中央空调控制面板里面的外挂电路，通过飞线连接到被控按钮的焊点上，
    通过飞线输出低电平脉冲来模拟按键，实现红外遥控特定按钮（电源开关、风力大小）的功能。
    AVR检测到飞线上的按钮按下并保持5秒钟，则启动对应飞线通道的学习过程，BEEP长鸣。
    在学习过程中，按任意键将退出学习，超过8秒没有收到有效的红外指令将结束学习过程。

    智能家居IOT场景中使用遥控开关，仅有乒乓开关的情况下，远程操控时，无法得知开关的实际状态，就无法盲操
    因此需要有固定开、固定关的指令。便于配合智能音箱遥控开、遥控关，这样可以放心地重复发送。可以盲操。
    解决办法是增加一条飞线POW_SEN到原面板电源LED指示灯上，让AVR能感知实际的开关状态。

    IOT方案是根据空调面板上电源开关飞线通道学习到的红外指令，派生出固定开和固定关的指令内码
        如学习到的红外按键的十六进制32位指令码（MSB）是 xxXXxxxx，根据第二字节XX的值派生
        XX+1的指令为按开，即 xxJJxxxx，其中JJ=XX+1，其余xx保持不变
        XX+2的指令为按关，即 xxKKxxxx，其中KK=XX+2，其余xx保持不变

    仅支持最常用的NEC格式的红外编码，每个按键指令有4字节的数据。
    NEC指令格式请参考：https://www.sbprojects.net/knowledge/ir/nec.php
    虽然NEC红外原定义是LSB格式（低位在前），但是开源IrRmote库直接使用32位无符号数表示
    一个内码（MSB高位在前）的程序代码很简洁方便，考虑到且IOT业务中常用IrRemote库解码红外指令，
    为了方便解码后的数据交流，本程序修改为MSB方式解码红外指令的每个字节。
   ======================================================================== */

#include <avr/io.h>
#include <avr/eeprom.h>
#include <util/delay_basic.h>
#include <avr/interrupt.h>
#include <string.h>

/*  --------------------------------------------------
    硬件引脚定义如有调整，请在这里修改
    红外接收器使用了INT0中断，所以只能固定在PB1，没得选。
    --------------------------------------------------  */
// 感知空调控制面板上“电源LED”状态的输入脚，带上拉，低有效
#define IO_POW_SEN              _BV(PB2)
// 飞线到空调面板上的电源按钮的IO，低有效
#define IO_POW_AN               _BV(PB3)
// 飞线到空调面板上的风力按钮的IO，低有效
#define IO_FAN_AN               _BV(PB0)
// 接个蜂鸣器，收到有效红外指令，吱一声，高有效。可用PB4
#define IO_BEEP                 _BV(PB4)

// 输出的模拟按键的个数
#define TOTAL_AN_COUNT          2
// 所有模拟按键的MASK码
#define KEYS_MASK               (IO_POW_AN | IO_FAN_AN)
// 各模拟按键对应的IO管脚，IO_POW_AN电源按钮要放到第一个
const unsigned char OutPinMask [TOTAL_AN_COUNT] = {IO_POW_AN, IO_FAN_AN};
// 模拟按钮低电平有效时的判断表达式
#define KEY_PRESSED             ( ~(PINB) & KEYS_MASK )
// 感知空调面板上的电源指示灯，判断实际状态
#define IS_POW_ON               ( ~(PINB) & IO_POW_SEN )

// 延时参数，输出脉冲宽度。1.2MHz时钟下，最大值65535理论对应218毫秒。推论：30061对应100毫秒，24050对应80毫秒
#define OUT_PULSE_WIDTH         30061
#define OUT_BEEP_WIDTH          20000
// 延时参数，影响进入学习状态前按住按钮所需的时间
// 从255递减到该值的时间  195: 约3秒钟  155: 约5秒钟
#define WAIT_LEARNING           155
// 延时参数，进入学习状态后的学习超时时间，从该值递减到零的时间，大约每秒递减20      160: 约8秒钟
#define DURLING_LEARNING        160

// 红外指令解码的状态机
typedef enum{
             IR_idle,
             IR_waitstart,
             IR_getaddrlow,
             IR_getaddrhigh,
             IR_getdata,
             IR_getdatainv
            }_IRstate;
volatile _IRstate IRstate = IR_idle;

//定义位操作
#define SET_BIT(reg,bitmask)    ( reg |=  (bitmask) )
#define CLR_BIT(reg,bitmask)    ( reg &= ~(bitmask) )
#define INV_BIT(reg,bitmask)    ( reg ^=  (bitmask) )
#define GET_BIT(reg,bitmask)    ( reg &   (bitmask) )
#define GET_PIN(bitmask)        ( PINB  &   (bitmask) )
#define OUT_HIGH(bitmask)       ( PORTB |=  (bitmask) )
#define OUT_LOW(bitmask)        ( PORTB &= ~(bitmask) )
#define OUT_INV(bitmask)        ( PORTB ^=  (bitmask) )

// 保存EEPROM的起始地址，根据datasheet描述的缺陷，不要从0地址开始使用
#define EE_MARGIN               0x10
#define START_T0                ( TCCR0B = 0x04 )
#define STOP_T0                 ( TCCR0B = 0x00 )
#define ENABLE_INT0             ( GIMSK  = 0x40 )
#define DISABLE_INT0            ( GIMSK  = 0x00 )
#define BEEP_ON                 ( OUT_HIGH(IO_BEEP) )
#define BEEP_OFF                ( OUT_LOW (IO_BEEP) )

volatile unsigned char IR_Ready;
volatile unsigned char TimerCount;

// NEC格式，每个红外指令有4个字节
typedef struct {
                unsigned char addrl;        // 地址低位
                unsigned char addrh;        // 地址高位
                unsigned char data;         // 指令码
                unsigned char datainv;      // 指令反码
                } _IRCMD;
volatile _IRCMD LearnedCMD[TOTAL_AN_COUNT];
volatile _IRCMD curCMD;

//----------------------------------------------------------------------------
// INTERUPT HANDLER (TIMER/COUNTER0 OVERFLOW)       void timer0_ovf_isr(void)
//----------------------------------------------------------------------------
ISR(TIM0_OVF_vect) {
    // 在内部默认的1.2MHz RC振荡器配置下，再256分频后，T0计满0xFF后的溢出周期约为53毫秒
    // 2011-1-15, 室温5摄氏度，实测周期为57.8毫秒
    if (! (-- TimerCount)) {
        IRstate = IR_idle;
        STOP_T0;
    }
}


// Attiny13缺省RC时钟1.2MHz，256分频后周期为206微秒，
// T0每206us计数加1，以下定义各时间段的计数值
//#define msec_15     0x48
//#define msec_12p5   0x3C
//#define msec_9      0x2B
//#define msec_2p5    0x0C
//#define msec_1p68   0x08
//#define msec_0p9    0x04

// Attiny13缺省RC时钟1.2MHz，256分频后周期为206微秒，
// T0每206us计数加1，以下定义各时间段的计数值
// 20130627, ATtiny13A比ATtiny13V的内置RC振荡器频率略低些，放宽这里的参数
// 以支持13A。正常情况下，TCNT0将采集到：
//             引导码=13.5mS; 逻辑1=2.25mS; 逻辑0=1.12mS; 重复码=11.25mS
// 以下定义各时间段的TCNT0的计数值
#define msec_15     0x48
#define msec_12p5   0x38
#define msec_9      0x2B
#define msec_2p5    0x0F
#define msec_1p68   0x08
#define msec_0p9    0x04

// 20250903，八十周年九三阅兵圆满完成。改为按照MSB格式解码，以迎合IOT中常用的开源IrRemote库的格式
// 原bits=0x01; 改为bits=0x80;    原bits=bits<<1; 改为bits=bits>>1;
//----------------------------------------------------------------------------
// INTERUPT EXTERMNAL INTERUPT 0
//----------------------------------------------------------------------------
ISR(INT0_vect) {
    static unsigned char bits;
    unsigned char time;
    switch(IRstate) {
        case IR_idle:
            TCNT0=0; START_T0; IRstate=IR_waitstart;
            TimerCount = 2;     // 最后一个脉冲后2个溢出周期(53ms)结束
            break;
        case IR_waitstart:
            // a "start of data" is 13.5Msec,a "1" is 2.25Msec,a "0" is 1.12 msec
            time=TCNT0; TCNT0=0;
            // greater than 12.5Msec & less than 15 msec = start code
            if ((time>msec_12p5)&&(time<msec_15)) {
                curCMD.addrl= curCMD.addrh= curCMD.data= curCMD.datainv= 0;
                bits=0x80; IRstate=IR_getaddrlow;
            }   else { IRstate=IR_idle;}  // too short, bad data just go to idle
            break;
        case IR_getaddrlow:
            time=TCNT0; TCNT0=0;
            // if  > 2.5msec or shorter than .9Msec bad data, go to idle
            if ((time>msec_2p5)||(time<msec_0p9)) {IRstate=IR_idle; break; }
            // MSB格式：接收到的第一个比特作为字节最高位
            if (time>msec_1p68)  curCMD.addrl|= bits;   // greater than 1.68Msec is a 1
            bits=bits>>1; if (!bits) {IRstate=IR_getaddrhigh; bits=0x80; }
            break;
        case IR_getaddrhigh:
            time=TCNT0; TCNT0=0;
            if ((time>msec_2p5)||(time<msec_0p9)) {IRstate=IR_idle; break; }
            if (time>msec_1p68)  curCMD.addrh|= bits;
            bits=bits>>1; if (!bits) {IRstate=IR_getdata; bits=0x80; }
            break;
        case IR_getdata:
            time=TCNT0; TCNT0=0;
            if ((time>msec_2p5)||(time<msec_0p9)) {IRstate=IR_idle; break; }
            if (time>msec_1p68)  curCMD.data|= bits;
            bits=bits>>1; if (!bits) {IRstate=IR_getdatainv; bits=0x80; }
            break;
        case IR_getdatainv:
            time=TCNT0; TCNT0=0;
            if ((time>msec_2p5)||(time<msec_0p9)) {IRstate=IR_idle; break; }
            if (time>msec_1p68)  curCMD.datainv|= bits;
            bits=bits>>1; if (!bits) {IR_Ready=1; IRstate=IR_idle; } // 完整的一帧数据结束
            break;
        default:
            IRstate=IR_idle;
            break;
    }
}

static void port_init(void) {
    // 蜂鸣器或者发光二极管指示灯，输出模式
    DDRB  = IO_BEEP;
    // PB1做INT0输入，电源感应教做输入,带上拉电阻；
    PORTB = _BV(PB1) | IO_POW_SEN | KEYS_MASK;
}

//TIMER0 initialize - prescale:256
// WGM: Normal
static void timer0_init(void) {
    //TCCR0B = 0x00; //stop
    STOP_T0;
    OCR0A = 0xEA;
    OCR0B = 0xEA;
    // TCNT0 = 0x16;    //set count
    TCCR0A = 0x00;
    //TCCR0B = 0x04; //start timer
}

static void init_devices(void) {
    cli();         //disable all interrupts
    port_init();
    timer0_init();
    MCUCR = 0x02;  // INT0下降沿触发
    TIMSK0 = 0x02; //timer interrupt sources
    // GIMSK = 0x40;  //interrupt sources
    ENABLE_INT0;
    sei();         //re-enable interrupts
}

void load_learned_instruction(void) {
    // void eeprom_read_block (void *__dst, const void *__src, size_t __n);
    eeprom_read_block((void *)&LearnedCMD, (void *)EE_MARGIN, sizeof(LearnedCMD));
}

void pulse_out(unsigned char Pin) {
    OUT_LOW(Pin);  SET_BIT(DDRB, Pin);  // IO口拉低，然后切换为输出状态
    _delay_loop_2(OUT_PULSE_WIDTH); 
    OUT_HIGH(Pin); CLR_BIT(DDRB, Pin);  // 完事后，先拉高，在切回输入状态
}

void beep_n1(void) {                // 1短鸣叫
    BEEP_ON; _delay_loop_2(OUT_BEEP_WIDTH); BEEP_OFF; _delay_loop_2(OUT_BEEP_WIDTH); 
}
void beep_m1(unsigned char x) {     // x长鸣叫
    BEEP_ON;
    for (unsigned char i=x; i; i--)
        _delay_loop_2(OUT_BEEP_WIDTH);
    BEEP_OFF;
}


//====================================================
// void __attribute__((noreturn)) main(void)
int main(void)
{
    unsigned char i, Key_State, cur_key_index = 0;
    init_devices();
    load_learned_instruction();     // 先读取学习到的指令数据
    while(1) {
        // ------------ 本机（原空调面板）按键按下 --------------------------------
        Key_State = KEY_PRESSED;
        if (Key_State) {
            DISABLE_INT0;           // 关INT0中断，暂停处理红外信号。红外中断函数会修改TimerCount，影响定时器
            TimerCount = 0xFF; START_T0;            // 启动定时器查看按钮按下的时长
            for (i=0; i<TOTAL_AN_COUNT; i++)        // 先搞清楚是按了哪个键
                if (Key_State == OutPinMask[i]) {cur_key_index=i; break; }

            while ((KEY_PRESSED) && (WAIT_LEARNING < TimerCount));     // 循环等按键放开
            if (WAIT_LEARNING >= TimerCount) {      // 如果是长按超过5秒，开始学习过程 
                TimerCount = 160;   // 设置8秒种超时，如果还没有收到任何信号，就放弃本次学习
                ENABLE_INT0;        // 开启INT0中断，等待学习红外信号，
                BEEP_ON;            // 打开蜂鸣器，开始鸣叫
                while (TimerCount) {// TimerCount由定时器递减，8秒后递减到0
                    //由于收到红外信号后会重新给TimerCount赋值2，所以无需再判断 IR_Ready 作为退出条件
                }
                if (IR_Ready) {     // 把学习到的指令数据保存到eeprom
                    IR_Ready = 0;
                    // void eeprom_write_block (const void *__src, void *__dst, size_t __n);
                    eeprom_write_block((void *)&curCMD, (void *)(EE_MARGIN + cur_key_index * sizeof(_IRCMD)), sizeof(_IRCMD));
                    load_learned_instruction();     // 更新当前RAM里的数据
                }
                BEEP_OFF;           // 关闭蜂鸣器
            }
            // 如果是短按按钮，原空调面板CPU处理，AVR不管。这里清理环境：尽快关闭定时器
            TimerCount = 1;         // 不要设置为0，防止 --TimerCount 后得 0xFF
            ENABLE_INT0;            // 开启INT0中断，允许处理红外信号
        }

        // ------------ 通常模式下，判断红外解码是否OK， ------------
        if (IR_Ready) {
            IR_Ready = 0; DISABLE_INT0;     // 暂停红外，杜绝数据被快速覆盖的可能性
            Key_State = IS_POW_ON;          // 采样保持当前空调的电源LED开关状态
            // 找到对应预学习的指令序号，来执行对应的动作
            for (i=0; i<TOTAL_AN_COUNT; i++) {
                // 找到对应的IO端口，输出低电平脉冲
/*              if ((curCMD.data  == LearnedCMD[i].data)  &&
                    (curCMD.addrh == LearnedCMD[i].addrh) &&
                    (curCMD.addrl == LearnedCMD[i].addrl)) {   */ 
                // 使用memcmp()内存比较函数,比上面的3个条件组合判断,节省至少14BYTE的FLASH空间
                if (memcmp((void*)&curCMD, (void*)&LearnedCMD[i], 4) == 0) {
                    BEEP_ON;
                    pulse_out(OutPinMask[i]);       // 输出低电平脉冲，相当于按了一下面板上的按键，
                    BEEP_OFF;
                    if (0 == i) {                   // 第一个AN是电源开关，需要IOT化处理，补第二声鸣叫
                        _delay_loop_2(OUT_BEEP_WIDTH);
                        if (Key_State)  beep_m1(6); // 原来是开的，按后新状态是电源关闭，鸣叫一短一长，补一长
                           else         beep_n1();  // 按按钮后，新状态是电源开：鸣叫两短，补一短
                    }
                    break;
                }
            }
            // IOT方案，根据电源按钮派生的内码来执行固开、固关的动作
            // if ((curCMD.data  == LearnedCMD[0].data)) {
            if ((curCMD.data  == LearnedCMD[0].data) && (curCMD.addrl == LearnedCMD[0].addrl)) {
                if (curCMD.addrh == LearnedCMD[0].addrh +1) {   // 固开
                    if (! Key_State) pulse_out(OutPinMask[0]);
                    beep_n1(); beep_n1();
                }
                if (curCMD.addrh == LearnedCMD[0].addrh +2) {   // 固关
                    if (Key_State)   pulse_out(OutPinMask[0]);
                    beep_n1(); beep_m1(6);
                }
            }
            ENABLE_INT0;        // 恢复INT0，启动红外接收
        }
    }
    return 0;
}
/* end of file */
